package bg.rsa_project.pi_calculator;

import java.awt.EventQueue;
import static java.awt.EventQueue.isDispatchThread;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JProgressBar;

/**
 *
 * @author Teofana Gadjeva
 * @author Nikola Pandurski
 */
public class PiCalculator {

    private static List<CachedValues> cache = new ArrayList<CachedValues>();
    private static int threads = 8;
    private static int precision = 1000;
    private static boolean inQuiteMode = false;
    private static String fileOutputPath = "result.txt";
    private static long time;
    private static JProgressBar pb;

    public static void main(String[] args) {
        for (String params : args) {
            if (params.matches("-p \\d*")) {
                precision = Integer.parseInt(params.substring(3, params.length()));
            } else if (params.matches("-t \\d*")) {
                threads = Integer.parseInt(params.substring(3, params.length()));
            } else if (params.matches("-q")) {
                inQuiteMode = true;
            } else if (params.matches("-o .*\\w*\\.\\w*")) {
                fileOutputPath = params.substring(3, params.length());
                System.out.println(fileOutputPath);
            }
        }

        BigDecimal pi = calculatePi(threads, precision, true, inQuiteMode);
        System.out.println("Threads: " + threads + "\nPrecision: " + precision);

        System.out.println("Pi = " + pi);
        System.out.println("Execution time: " + time + " milliseconds");

    }

    public static void setFileOutputPath(String fileOutputPath) {
        PiCalculator.fileOutputPath = fileOutputPath;
    }

    public static JProgressBar getPb() {
        return pb;
    }

    public static long getTime() {
        return time;
    }

    public static void setTime(long time) {
        PiCalculator.time = time;
    }

    public static BigDecimal calculatePi(int threads, int precision, boolean writeOnFile, boolean quiteMode) {
        inQuiteMode = quiteMode;
        long start = System.currentTimeMillis();
        BigDecimal overallSum = calculateSums(threads, precision);
        BigDecimal pi = new BigDecimal(3528).divide(overallSum, precision,
                BigDecimal.ROUND_HALF_UP);
        time = (System.currentTimeMillis() - start);
        if (writeOnFile) {
            PrintWriter writer = null;
            try {
                writer = new PrintWriter(fileOutputPath, "UTF-8");
                writer.println("Threads: " + threads + "\n Precision: " + precision);
                writer.println("Pi = " + pi);
                writer.println("Execution time: " + time + " milliseconds");

            } catch (FileNotFoundException ex) {
                Logger.getLogger(PiCalculator.class.getName()).log(Level.SEVERE, null, ex);
            } catch (UnsupportedEncodingException ex) {
                Logger.getLogger(PiCalculator.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                writer.close();
            }
        }
        return pi;
    }

    /**
     * With this method we calculate the sum in Ramanujan's formula.
     */
    private static BigDecimal calculateSumbyStep(BigInteger numOfTerms,
            BigInteger step, BigInteger startNumber, boolean firstThread) {
        BigInteger n = startNumber;
        BigDecimal sum = new BigDecimal(0);
        while (n.compareTo(numOfTerms) < 1) {
            final int i = n.intValue();
            final double terms = numOfTerms.doubleValue();
//            System.out.println(i);
            if (!inQuiteMode && firstThread) {
                System.out.println((int) ((i / numOfTerms.doubleValue()) * 100) + "%");
                if (pb != null) {
                    System.out.println(isDispatchThread());
                    EventQueue.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            pb.setValue((int) ((i / terms) * 100));
                        }
                    });

                }
            }
//             
            BigDecimal firstDividend = new BigDecimal(
                    ((i % 2 == 0) ? new BigInteger("1") : new BigInteger("-1"))//substitutes pow
                    .multiply(factorialChooser(startNumber.intValue(), new BigInteger(Integer
                                            .toString(4 * i)), true)));
            BigDecimal firstDivisor = new BigDecimal(
                    (fourNCalculation(startNumber.intValue(), i).multiply(factorialChooser(startNumber.intValue(), n, false))).pow(4));
            BigDecimal secondDividend = new BigDecimal(
                    (new BigInteger("21460").multiply(n)).add(new BigInteger(
                                    "1123")));
            BigDecimal secondDivisor = new BigDecimal(eightThCalculation(startNumber.intValue(), i));

            BigDecimal result = firstDividend.multiply(secondDividend)
                    .divide(firstDivisor, 1000, BigDecimal.ROUND_HALF_UP)
                    .divide(secondDivisor, 1000, BigDecimal.ROUND_HALF_UP);
            sum = sum.add(result);
            n = n.add(step);
        }

        return sum;
    }

    /**
     * Multithreaded calculation of the sum in Ramanujan's formula.
     */
    public static BigDecimal calculateSums(int numOfThreads, int precision) {
        BigDecimal sum = new BigDecimal("0");
        final List<BigDecimal> sumList = new ArrayList<BigDecimal>(numOfThreads);
        final List<Boolean> readyFlags = new ArrayList<Boolean>(numOfThreads);
        for (int i = 0; i < numOfThreads; i++) {
            readyFlags.add(false);
        }
        for (int i = 0; i < numOfThreads; i++) {
            cache.add(i, new CachedValues());
            final BigInteger startNumber = new BigInteger(Integer.toString(i));
            final BigInteger step = new BigInteger(
                    Integer.toString(numOfThreads));
            final BigInteger numOfTerms = new BigInteger(
                    Integer.toString(precision));
            new Thread(new Runnable() {
                @Override
                public void run() {
                    BigDecimal partialSum;
                    if (startNumber.intValue() == 0) {
                        partialSum = calculateSumbyStep(numOfTerms,
                                step, startNumber, true);
                    } else {
                        partialSum = calculateSumbyStep(numOfTerms,
                                step, startNumber, false);
                    }

                    sumList.add(partialSum);
                    readyFlags.set(startNumber.intValue(), true);

                }
            }).start();
        }
        while (readyFlags.contains(false)) {
        }
        for (BigDecimal partialSum : sumList) {
            sum = sum.add(partialSum);
        }
        return sum;
    }

    private static BigInteger fourNCalculation(int numOfCallerThread, int n) {
        BigInteger currentFourN = cache.get(numOfCallerThread).getFourN();
        Integer currentFourNKey = cache.get(numOfCallerThread).getFourNKey();
        while (currentFourNKey < n) {
            currentFourN = currentFourN.multiply(new BigInteger("4"));
            currentFourNKey = currentFourNKey + 1;
        }
        cache.get(numOfCallerThread).setFourN(currentFourN);
        cache.get(numOfCallerThread).setFourNKey(currentFourNKey);
        return currentFourN;
    }

    private static BigInteger eightThCalculation(int numOfCallerThread, int n) {
        BigInteger currentEightTh = cache.get(numOfCallerThread).getEightTh();
        Integer currentEightThKey = cache.get(numOfCallerThread).getEightThKey();
        while (currentEightThKey < 2 * n) {
            currentEightTh = currentEightTh.multiply(new BigInteger("882"));
            currentEightThKey = currentEightThKey + 1;
        }
        cache.get(numOfCallerThread).setEightTh(currentEightTh);
        cache.get(numOfCallerThread).setEightThKey(currentEightThKey);
        return currentEightTh;
    }

    private static BigInteger factorialChooser(int numOfCallerThread, BigInteger n, boolean bigFac) {
        if (bigFac) {
            CachedValues temp = cache.get(numOfCallerThread);
            BigInteger result = factorial(temp.getBigFac(), temp.getBigFacKey(), n);
            temp.setBigFac(result);
            temp.setBigFacKey(n.intValue());
            return result;
        } else {
            CachedValues temp = cache.get(numOfCallerThread);
            BigInteger result = factorial(temp.getSmallFac(), temp.getSmallFacKey(), n);
            temp.setSmallFac(result);
            temp.setSmallFacKey(n.intValue());
            return result;
        }
    }

    /**
     * Simple helper method to calculate factorial of n
     */
    private static BigInteger factorial(BigInteger calculated, Integer toNum, BigInteger n) {
        BigInteger result = new BigInteger("1");
//            
        while (n.compareTo(new BigInteger("1")) > 0) {
            if (n.intValue() - 1 == toNum) {
                return result.multiply(n).multiply(calculated);
            }
            result = result.multiply(n);
            n = n.subtract(new BigInteger("1"));
        }
        return result;
    }

    static void setProgressBar(JProgressBar progress) {
        pb = progress;
    }
}

class CachedValues {

    BigInteger smallFac;
    Integer smallFacKey;

    BigInteger bigFac;
    Integer bigFacKey;

    BigInteger eightTh;
    Integer eightThKey;

    BigInteger fourN;
    Integer fourNKey;

    public CachedValues() {
        smallFac = new BigInteger("1");
        smallFacKey = new Integer("0");

        bigFac = new BigInteger("1");
        bigFacKey = new Integer("0");

        eightTh = new BigInteger("1");
        eightThKey = new Integer("0");

        fourN = new BigInteger("1");
        fourNKey = new Integer("0");
    }

    public void setKey(Integer newKey) {
        smallFacKey = newKey;
    }

    public Integer getKey() {
        return smallFacKey;
    }

    public BigInteger getSmallFac() {
        return smallFac;
    }

    public void setSmallFac(BigInteger smallFac) {
        this.smallFac = smallFac;
    }

    public Integer getSmallFacKey() {
        return smallFacKey;
    }

    public void setSmallFacKey(Integer smallFacKey) {
        this.smallFacKey = smallFacKey;
    }

    public BigInteger getBigFac() {
        return bigFac;
    }

    public void setBigFac(BigInteger bigFac) {
        this.bigFac = bigFac;
    }

    public Integer getBigFacKey() {
        return bigFacKey;
    }

    public void setBigFacKey(Integer bigFacKey) {
        this.bigFacKey = bigFacKey;
    }

    public BigInteger getEightTh() {
        return eightTh;
    }

    public void setEightTh(BigInteger eightTh) {
        this.eightTh = eightTh;
    }

    public Integer getEightThKey() {
        return eightThKey;
    }

    public void setEightThKey(Integer eightThKey) {
        this.eightThKey = eightThKey;
    }

    public BigInteger getFourN() {
        return fourN;
    }

    public void setFourN(BigInteger fourN) {
        this.fourN = fourN;
    }

    public Integer getFourNKey() {
        return fourNKey;
    }

    public void setFourNKey(Integer fourNKey) {
        this.fourNKey = fourNKey;
    }
}
